import hashlib
import json
from time import time
from urllib.parse import urlparse
from uuid import uuid4

import requests
from flask import Flask, jsonify, request

#区块链类
"""
我们的区块链类class负责管理区块的链接,它将存储交易
并具有一些用于将新块添加到链中的辅助方法
"""
class Blockchain:
    #构造函数创建一个初始的空列表(用于存储我们的区块链)
    def __init__(self):
        self.current_transactions = []
        self.chain = []
        self.nodes = set()

        #创建创世区块
        self.new_block(previous_hash='1', proof=100)

    """
    将新节点添加到节点列表中
    param address：节点的地址
    """
    def register_node(self, address):
        parsed_url = urlparse(address)
        if parsed_url.netloc:
            self.nodes.add(parsed_url.netloc)
        elif parsed_url.path:
            #接受没有类似"192.168.0.5:5000"的方案的URL
            self.nodes.add(parsed_url.path)
        else:
            raise ValueError('Invalid URL')

    
    """
    确定给定的区块链是否有效
    param chain：区块链
    return：如果有效，则返回True；如果无效，则返回False
    """
    def valid_chain(self, chain):
        last_block = chain[0]
        current_index = 1
        while current_index < len(chain):
            block = chain[current_index]
            print(f'{last_block}')
            print(f'{block}')
            print("\n-----------\n")
            #检查块的哈希是否正确
            last_block_hash = self.hash(last_block)
            if block['previous_hash'] != last_block_hash:
                return False

            #检查工作证明是否正确
            if not self.valid_proof(last_block['proof'], block['proof'], last_block_hash):
                return False

            last_block = block
            current_index += 1

        return True

    """
    这是我们的共识
    用网络中最长的链条代替我们的链条
    return：如果更换了链,则返回True,如果没有,则返回False
    """
    def resolve_conflicts(self):
        neighbours = self.nodes
        new_chain = None

        #我们只想找比我们长的链子
        max_length = len(self.chain)

        #抓取并验证我们网络中所有节点的链
        for node in neighbours:
            response = requests.get(f'http://{node}/chain')

            if response.status_code == 200:
                length = response.json()['length']
                chain = response.json()['chain']

                #检查长度是否较长，链条是否有效
                if length > max_length and self.valid_chain(chain):
                    max_length = length
                    new_chain = chain

        #如果我们发现一个新的,有效的链比我们的长,请更换我们的链
        if new_chain:
            self.chain = new_chain
            return True

        return False
    
    """
    在区块链中创建新区块
    param proof：工作证明算法给出的证明
    param previous_hash：上一个块的哈希值
    return: 新建块
    """
    def new_block(self, proof, previous_hash):
        block = {
            'index': len(self.chain) + 1,
            'timestamp': time(),
            'transactions': self.current_transactions,
            'proof': proof,
            'previous_hash': previous_hash or self.hash(self.chain[-1]),
        }

        #重置当前事务列表
        self.current_transactions = []

        self.chain.append(block)
        return block

    """
    将交易添加到区块的方法
    param sender:发件人的地址
    param recipient:收件人的地址
    param amount：金额
    return：将保存此事务的块的索引
    """
    def new_transaction(self, sender, recipient, amount):
        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        return self.last_block['index'] + 1

    @property
    def last_block(self):
        return self.chain[-1]

    """
    创建块的SHA-256哈希
    param block：块
    """
    @staticmethod
    def hash(block):
        #我们必须确保字典是有序的,否则我们会有不一致的散列
        block_string = json.dumps(block, sort_keys=True).encode()
        return hashlib.sha256(block_string).hexdigest()

    """
    简单的工作证明算法
    找到一个数字p'，使得散列(pp')包含前导4个零
    其中p是以前的证明,p'是新的证明
    param last块：<dict>last块
    return: <int>
    """
    def proof_of_work(self, last_block):
        last_proof = last_block['proof']
        last_hash = self.hash(last_block)

        proof = 0
        while self.valid_proof(last_proof, proof, last_hash) is False:
            proof += 1

        return proof

    """
    验证证据
    param last_proof: <int>上一个证明
    param proof: <int>当前证明
    param last_hash: <str>上一个块的哈希
    return: <bool>如果正确则为True，如果不正确则为False。
    """
    @staticmethod
    def valid_proof(last_proof, proof, last_hash):
        guess = f'{last_proof}{proof}{last_hash}'.encode()
        guess_hash = hashlib.sha256(guess).hexdigest()
        return guess_hash[:4] == "0000"


#实例化节点
app = Flask(__name__)

#为此节点生成全局唯一地址
node_identifier = str(uuid4()).replace('-', '')

#实例化区块链
blockchain = Blockchain()


@app.route('/mine', methods=['GET'])
def mine():
    #我们运行工作证明算法来得到下一个证明
    last_block = blockchain.last_block
    proof = blockchain.proof_of_work(last_block)

    #找到证据我们必须得到奖励
    #发送方为“0”,表示此节点已挖掘新BitCoins
    blockchain.new_transaction(
        sender="0",
        recipient=node_identifier,
        amount=1,
    )

    #通过将新块添加到链中来锻造新块
    previous_hash = blockchain.hash(last_block)
    block = blockchain.new_block(proof, previous_hash)

    response = {
        'message': "New Block Forged",
        'index': block['index'],
        'transactions': block['transactions'],
        'proof': block['proof'],
        'previous_hash': block['previous_hash'],
    }
    return jsonify(response), 200


@app.route('/transactions/new', methods=['POST'])
def new_transaction():
    values = request.get_json()

    #检查所需字段是否在发布的数据中
    required = ['sender', 'recipient', 'amount']
    if not all(k in values for k in required):
        return 'Missing values', 400

    #创建新交易
    index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])

    response = {'message': f'Transaction will be added to Block {index}'}
    return jsonify(response), 201


@app.route('/chain', methods=['GET'])
def full_chain():
    response = {
        'chain': blockchain.chain,
        'length': len(blockchain.chain),
    }
    return jsonify(response), 200


@app.route('/nodes/register', methods=['POST'])
def register_nodes():
    values = request.get_json()

    nodes = values.get('nodes')
    if nodes is None:
        return "Error: Please supply a valid list of nodes", 400

    for node in nodes:
        blockchain.register_node(node)

    response = {
        'message': 'New nodes have been added',
        'total_nodes': list(blockchain.nodes),
    }
    return jsonify(response), 201


@app.route('/nodes/resolve', methods=['GET'])
def consensus():
    replaced = blockchain.resolve_conflicts()

    if replaced:
        response = {
            'message': 'Our chain was replaced',
            'new_chain': blockchain.chain
        }
    else:
        response = {
            'message': 'Our chain is authoritative',
            'chain': blockchain.chain
        }

    return jsonify(response), 200


if __name__ == '__main__':
    from argparse import ArgumentParser

    parser = ArgumentParser()
    parser.add_argument('-p', '--port', default=5000, type=int, help='port to listen on')
    args = parser.parse_args()
    port = args.port

    app.run(host='0.0.0.0', port=port)
